%% Copyright
-module(couchdb_catalog).
-author("").

-define(app_design_doc, <<"catalog">>).
-define(product_view, <<"product">>).
-define(review_view, <<"review">>).
-define(tag_view, <<"tag">>).
-define(id_field, <<"_id">>).
-define(rev_field, <<"_rev">>).
-define(doc_field, <<"doc">>).
-define(type_field, <<"type">>).
-define(product_type, <<"product">>).
-define(review_type, <<"review">>).
-define(prod_id_field, <<"prodId">>).

-behaviour(gen_server).

%% API
-export([prepare_product_doc/1]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

%%====================================================================
%% gen_server callbacks
%%====================================================================

init({Address, Port, Database}) ->
  Server = couchbeam:server_connection(Address, Port, "", []),
  {ok, Db} = couchbeam:open_or_create_db(Server, Database),
  DocId = <<"_design/", ?app_design_doc/binary>>,
  case couchbeam:doc_exists(Db, DocId) of
    true -> {ok, Db};
    false ->
      DesignDoc = get_design_doc(DocId),
      {ok, _SavedDoc} = couchbeam:save_doc(Db, DesignDoc),
      {ok, Db}
  end.

handle_call({product_exists, ProductId}, _From, Db) ->
  Reply = product_exists(Db, ProductId),
  {reply, Reply, Db};

handle_call({add_product, Product}, _From, Db) ->
  {ok, Key} = add_product(Db, Product),
  {reply, {ok, Key}, Db};

handle_call({get_product, ProductId}, _From, Db) ->
  Reply = get_product(Db, ProductId),
  {reply, Reply, Db};

handle_call({get_product_details, ProductId}, _From, Db) ->
  Reply = get_product_details(Db, ProductId),
  {reply, Reply, Db};

handle_call({add_product_review, ProductId, Review}, _From, Db) ->
  Reply = add_product_review(Db, ProductId, Review),
  {reply, Reply, Db};

handle_call({review_exists, ReviewId}, _From, Db) ->
  Reply = review_exists(Db, ReviewId),
  {reply, Reply, Db};

handle_call({get_product_review, ProductId, ReviewId}, _From, Db) ->
  Reply = get_product_review(Db, ProductId, ReviewId),
  {reply, Reply, Db};

handle_call({update_product_review, ProductId, ReviewId, ReviewUpdate}, _From, Db) ->
  {ok, Key} = update_product_review(Db, ProductId, ReviewId, ReviewUpdate),
  {reply, {ok, Key}, Db};

handle_call({get_product_reviews, ProductId, PageIndex, PageSize}, _From, Db) ->
  Reply = get_product_reviews(Db, ProductId, PageIndex, PageSize),
  {reply, Reply, Db};

handle_call({get_tags}, _From, Db) ->
  {reply, {ok, get_tags(Db)}, Db};

handle_call({search_product, id, Keys}, _From, Db) ->
  FoldFun = fun(ProductId, Acc) ->
    case get_product(Db, ProductId) of
      {ok, Product} -> [Product | Acc];
      {error, not_found} -> Acc
    end
  end,
  {reply, {ok, lists:foldl(FoldFun, [], Keys)}, Db};

handle_call({search_product, tag, Tag, {PageIndex, PageSize}}, _From, Db) ->
  {ok, Products} = get_products_by_tag(Db, Tag, PageIndex, PageSize),
  {reply, {ok, Products}, Db};

handle_call({search_product, name, Name, {PageIndex, PageSize}}, _From, CatalogContext) ->
  {reply, {ok, [{<<"totalCount">>, <<"0">>}, {<<"products">>, <<"">>}]}, CatalogContext};

handle_call({delete_all}, _From, Db) ->
  ok = delete_all_products(Db),
  {reply, {ok, deleted}, Db}.

handle_cast(_Msg, State) ->
  {noreply, State}.

handle_info(_Info, State) ->
  {noreply, State}.

terminate(_Reason, _State) ->
  ok.

code_change(_OldVer, State, _Extra) ->
  {ok, State}.

%%====================================================================
%% Implementation
%%====================================================================

get_design_doc(DocId) ->
  {[{?id_field, DocId},
    {<<"language">>, <<"javascript">>},
    {<<"views">>, get_design_doc_views()}]}.

get_design_doc_views() ->
  {[{<<"product">>, {[{<<"map">>, <<"function(doc){\n if (doc.type && doc.type == \"product\") {\n   emit(doc._id, doc._rev) }\n}">>}]}},
   {<<"review">>, {[{<<"map">>, <<"function(doc){\n if (doc.type && doc.type == \"review\") {\n   emit(doc.prodId, doc._rev) }\n}">>}]}},
   {<<"tag">>, {[{<<"map">>, <<"function(doc){\n if (doc.type && doc.type == \"product\" && doc.tags && doc.tags.length > 0) {\n for(var idx in doc.tags) { emit(doc.tags[idx], doc._id) } }\n}">>}]}}]}.

add_product(Db, Product) ->
  ProductDoc = couchbeam_doc:extend(?type_field, ?product_type, {Product}),
  {ok, SavedProductDoc} = couchbeam:save_doc(Db, ProductDoc),
  {ok, couchbeam_doc:get_id(SavedProductDoc)}.

get_product(Db, ProductId) ->
  open_decode_doc(Db, ?product_type, ProductId, fun prepare_product_doc/1).

get_product_details(Db, ProductId) ->
  open_decode_doc(Db, ?product_type, ProductId, fun prepare_product_details_doc/1).

delete_all_products(Db) ->
  DesignDocId = <<"_design/", ?app_design_doc/binary>>,
  ProcessFun = fun(Row) ->
                  case couchbeam_doc:get_value(<<"id">>, Row) of
                    DesignDocId -> ok;
                    Id ->
                      case couchbeam:open_doc(Db, Id) of
                        {ok, Doc} -> couchbeam:delete_doc(Db, Doc, [{empty_on_delete, true}]);
                        _ -> ok
                      end
                  end
               end,
  couchbeam_view:foreach(ProcessFun, Db, 'all_docs'),
  ok.

add_product_review(Db, ProductId, Review) ->
  case couchbeam:doc_exists(Db, ProductId) of
    true ->
      ReviewDoc = couchbeam_doc:extend([{?type_field, ?review_type}, {?prod_id_field, ProductId}], {Review}),
      {ok, StoredDoc} = couchbeam:save_doc(Db, ReviewDoc),
      {ok, couchbeam_doc:get_id(StoredDoc)};

    false ->
      {error, not_found}
  end.

product_exists(Db, ProductId) ->
  document_exists(Db, ?product_type, ProductId).

review_exists(Db, ReviewId) ->
  document_exists(Db, ?review_type, ReviewId).

document_exists(Db, Type, DocumentId) ->
  case open_decode_doc(Db, Type, DocumentId, fun(Doc) -> Doc end) of
    {ok, _Doc} -> true;
    _ -> false
  end.

get_product_review(Db, ProductId, ReviewId) ->
  case couchbeam:doc_exists(Db, ProductId) of
    true -> open_decode_doc(Db, ?review_type, ReviewId, fun prepare_product_review_doc/1);
    false -> {error, not_found}
  end.

update_product_review(Db, ProductId, ReviewId, ReviewUpdate) ->
  ProductDocExists = couchbeam:doc_exists(Db, ProductId),
  ReviewDocExists = couchbeam:doc_exists(Db, ReviewId),

  case (ProductDocExists and ReviewDocExists) of
    true ->
      {ok, OldReviewDoc} = couchbeam:open_doc(Db, ReviewId),
      {Id, Rev} = couchbeam_doc:get_idrev(OldReviewDoc),

      Properties = [{?id_field, Id},
                    {?rev_field, Rev},
                    {?type_field, ?review_type},
                    {?prod_id_field, ProductId}],

      ReviewDoc = couchbeam_doc:extend(Properties, {ReviewUpdate}),

      {ok, StoredDoc} = couchbeam:save_doc(Db, ReviewDoc),
      {ok, couchbeam_doc:get_id(StoredDoc)};

    false -> {error, not_found}
  end.

get_product_reviews(Db, ProductId, PageIndex, PageSize) ->
  case couchbeam:doc_exists(Db, ProductId) of
    true ->
      Options = [{key, ProductId},
                 {skip, PageIndex * PageSize},
                 {limit, PageSize},
                 include_docs],

      {ok, Docs} = couchbeam_view:fetch(Db, {?app_design_doc, ?review_view}, Options),
      {ok, AllDocs} = couchbeam_view:fetch(Db, {?app_design_doc, ?review_view}, [{key, ProductId}]),

      FoldFun = fun(Doc, Acc) ->
        ReviewDocRaw = couchbeam_doc:get_value(?doc_field, Doc),
        ReviewDoc = prepare_product_review_doc(ReviewDocRaw),
        [decode_doc(ReviewDoc) | Acc]
      end,

      {ok, [{<<"totalCount">>, binary_util:integer_to_binary(lists:flatlength(AllDocs))},
            {<<"reviews">>, lists:foldl(FoldFun, [], Docs)}]};
    false ->
      {error, not_found}
  end.

get_tags(Db) ->
  {ok, TagObjects} = couchbeam_view:fetch(Db, {?app_design_doc, ?tag_view}, []),
  FoldFun = fun(Doc, Acc) ->
              [couchbeam_doc:get_value(<<"key">>, Doc) | Acc ]
            end,
  Tags = lists:foldl(FoldFun, [], TagObjects),
  lists:usort(Tags).

get_products_by_tag(Db, Tag, PageIndex, PageSize) ->
  Options = [{skip, PageIndex * PageSize},
             {limit, PageSize},
             {key, Tag}],
  {ok, TagObjects} = couchbeam_view:fetch(Db, {?app_design_doc, ?tag_view}, Options),
  {ok, AllDocs} = couchbeam_view:fetch(Db, {?app_design_doc, ?tag_view}, [{key, Tag}]),

  FoldFun = fun(Doc, Acc) ->
    ProductId = couchbeam_doc:get_value(<<"value">>, Doc),
    {ok, Product} = get_product(Db, ProductId),
    [Product | Acc]
  end,
  Products = lists:reverse(lists:foldl(FoldFun, [], TagObjects)),

  {ok, [{<<"totalCount">>, lists:flatlength(AllDocs)}, {<<"products">>, Products}]}.

prepare_product_details_doc(Doc) ->
  Id = couchbeam_doc:get_id(Doc),
  Doc1 = prepare_doc(Doc),
  couchbeam_doc:extend({?prod_id_field, Id}, Doc1).

prepare_product_doc(Doc) ->
  ProdId = couchbeam_doc:get_id(Doc),
  Tags = couchbeam_doc:get_value(<<"tags">>, Doc, list_to_binary([])),
  ProdName = couchbeam_doc:get_value(<<"prodName">>, Doc, <<"no name">>),
  ProdDesc = couchbeam_doc:get_value(<<"prodDesc">>, Doc, <<"no description">>),
  ProdPrice = couchbeam_doc:get_value(<<"prodPrice">>, Doc, <<"0.0">>),
  ImageUrl = couchbeam_doc:get_value(<<"imageUrl">>, Doc, <<"">>),
  {[{?prod_id_field, ProdId},
   {<<"tags">>, Tags},
   {<<"prodName">>, ProdName},
   {<<"prodDesc">>, ProdDesc},
   {<<"prodPrice">>, ProdPrice},
   {<<"imageUrl">>, ImageUrl}]}.

prepare_product_review_doc(Doc) ->
  ReviewId = couchbeam_doc:get_id(Doc),
  Doc1 = prepare_doc(Doc),
  couchbeam_doc:extend({<<"reviewId">>, ReviewId}, Doc1).

prepare_doc(Doc) ->
  DocRemovedRev = couchbeam_doc:delete_value(?rev_field, Doc),
  DocRemovedId = couchbeam_doc:delete_value(?id_field, DocRemovedRev),
  couchbeam_doc:delete_value(?type_field, DocRemovedId).

open_decode_doc(Db, Type, DocumentId, PostProcessFun) ->
  case couchbeam:open_doc(Db, DocumentId) of
    {ok, Doc} -> check_type_and_decode(Type, Doc, PostProcessFun);
    NotFound -> NotFound
  end.

check_type_and_decode(Type, Doc, PostProcessFun) ->
  case couchbeam_doc:get_value(?type_field, Doc) of
    Type -> {ok, decode_doc(PostProcessFun(Doc))};
    _ -> {error, not_found}
  end.

decode_doc(Doc) ->
  {ListOfProperties} = Doc,
  ListOfProperties.